Skip to content

[WIP][POC] Pgaur on pair evaluation - #50791

Draft
prtkgaur wants to merge 24 commits into
apache:mainfrom
prtkgaur:pgaurOnPairEvaluation
Draft

[WIP][POC] Pgaur on pair evaluation#50791
prtkgaur wants to merge 24 commits into
apache:mainfrom
prtkgaur:pgaurOnPairEvaluation

Conversation

@prtkgaur

@prtkgaur prtkgaur commented Aug 3, 2026

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is your first pull request you can find detailed information on how to contribute here:

Please remove this line and the above text before creating your pull request.

Rationale for this change

What changes are included in this PR?

Are these changes tested?

Are there any user-facing changes?

This PR includes breaking changes to public APIs. (If there are any breaking changes to public APIs, please explain which changes are breaking. If not, you can remove this.)

This PR contains a "Critical Fix". (If the changes fix either (a) a security vulnerability, (b) a bug that caused incorrect or invalid data to be produced, or (c) a bug that causes a crash (even when the API contract is upheld), please provide explanation. If not, you can remove this.)

ArnavBalyan and others added 21 commits November 23, 2025 14:05
A C++ implementation of the OnPair short-string codec (arXiv:2508.02280):
a trained dictionary of up to 16-byte tokens, greedy longest-prefix
tokenization, and a branch-free gather-copy decode that keeps per-row
random access. The dictionary budget is configurable from 9 to 16 bits;
codes are bit-packed at the dictionary's true width.
Splits each value into a shared prefix and a suffix before the symbol
table sees it, following the FSST+ thesis (Alexandre, CWI 2025). Used to
measure whether prefix extraction adds anything on top of either codec.
Measures FSST, OnPair, the prefix-extraction variants and zstd/lz4 pages
on one corpus set, on three axes: compression ratio, whole-column decode
with per-row random access, and encode. Every codec is charged a
bit-packed per-row length array so column reconstruction costs the same
across all of them. bench_common.h holds the timing and bit-packing
helpers shared with the cascade benchmark.

The Rust helper generates the corpora: TPC-H string columns, the OnPair
paper's real-world datasets, ClickBench columns, and synthetic
identifier and JSON sets.
Measures the native Parquet pages as libparquet writes them, each on its
own and followed by zstd(1) or lz4, against FSST and OnPair with and
without a generic codec on top. Also measures the dictionary-then-OnPair
cascade, which dictionary-encodes the column and OnPairs only the
distinct values.

Native pages are charged only what the writer emits, since they carry
their own lengths; the candidate codecs are charged a separate length
array. Auto-budget selection runs in per-corpus setup, outside the timed
region, as every codec's parameter choice does.
The roundtrip checker now runs every dictionary budget from 9 to 16
rather than only 16, for both the plain and the dedup layout, and exits
non-zero on any mismatch so it can gate a benchmark run. The packed
decode loop is templated on the code width and the auto budget picks a
width per column, so checking only 16 left the width the benchmarks
report unverified.

The width sweep answers whether a wider code decodes faster without the
confound a budget sweep carries. It holds one trained dictionary and its
code stream fixed and re-packs the same codes at every width up to 16,
so tokens, token count and copy width are identical and only the
unpacking differs.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Thanks for opening a pull request!

This pull request has been automatically converted to a draft because its title doesn't match Arrow's required format.

If this is not a minor PR. Could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose

Opening GitHub issues ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename the pull request title in the following format?

GH-${GITHUB_ISSUE_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

After updating the title, you can mark the pull request as ready for review.

See also:

The comment above the decode loop claimed it was store-bandwidth-bound, on the
evidence that (over-copy factor) x (decode MiB/s) was constant across corpora.
That product is equally constant when the loop is bound by tokens retired, since
over-copy is kCopy/mean_token_len and throughput is mean_token_len x tokens/s --
the two models are indistinguishable from that measurement, and the wrong one was
picked. It then steered two experiments (block-wise unpacking with prefetch, a
narrower two-store copy) at a cost that was not the constraint.

An ablation ladder separates them. Deleting the stores makes the loop slower;
deleting the random dictionary read nearly doubles it. The cost is the gather: the
stored layout reads a u32 offsets pair from one array and the payload from a
variable-stride blob, so every token touches two independent random cache lines.
FSST's decoder never pays this -- it reads a fixed-stride symbol[] plus len[].

Add StridedDictionary, a decode-side view giving each token a fixed 16-byte slot
and a length byte in a dense side array, so a token is one cache line. It is
deliberately not the stored form: ~17 bytes per token against ~12 would add
hundreds of KiB to a 65k-token dictionary and move every ratio. It is built once
per column from an unchanged CompactDictionary and thrown away, costing nothing on
disk. Skipped when the code stream is too short to amortise the O(tokens) build.

Where a predicated store is available the loop writes exactly one token's length,
which removes the fixed over-copy and collapses the 4/8/16 width dispatch to a
single kernel per code width. The guard is compile-time on purpose: the portable
path is byte-identical and only slower, so a build without it loses speed and
nothing else. The strided load alone, with today's fixed-width store, is worth
most of the win and needs no ISA feature.

DecompressPacked keeps its signature and builds the view internally; a new
overload takes a view the caller built once, for a reader decoding many pages
against one dictionary.

verify_roundtrip gains a byte-exact gate gating all four decode paths -- whole
column, self-building packed, prebuilt view, and the short-stream fallback --
against a scalar reference, since nothing else reaches the last of those.

Verified under -march=native and the portable fallback: 30 corpora, 86,400,060
row comparisons, 120 path gates, no disagreements under either. Compressed sizes
and ratios are bit-identical to the published ladder run on all shared rows.
At a fixed code width the (byte offset, intra-byte shift) pair a packed code
is read with repeats with a period of 8 / gcd(width, 8) codes. Unrolling the
decode loop by a whole multiple of that period makes every offset and every
shift a compile-time constant and advances the stream cursor once per group
rather than once per code.

The group size is a request that is rounded up to a whole period, so it is a
floor rather than the group emitted. That is what lets one setting be
constant-addressed at every width including 16, where the period is a single
code and unrolling by the period alone would be a no-op -- which is exactly
the high-cardinality half of the corpus set.

Measured with the builds alternating many times over, pinned, order rotated
and reversed between repetitions, every comparison a paired difference taken
inside one repetition: +4.6% on the corpus-set median with the predicated
store and +20.7% on the portable build, +34% on the two 14-bit columns where
it removes a defect in the old loop rather than raising a ceiling. Nothing
about the compressed form is touched, so no ratio moves.

The verifier gains a sweep over sixteen consecutive stream lengths per
column, which covers every remainder any group size up to sixteen can leave
after its last whole group. A column whose code count is a whole multiple of
the group never reaches the remainder path, so a whole-column check alone
cannot see a fault there: with the remainder loop deliberately made to skip
its first code, 43 of the 120 checkpoints are caught by the length sweep and
by nothing else. An empty corpus is now a failure rather than a vacuous pass.
…Pair

The reference FSST trainer cannot be widened in place. Its pair counter is a
dense square over the code space, which is a few hundred kilobytes at nine bits
and tens of gigabytes at sixteen, and its symbol type packs the bytes into a
single machine word, capping a symbol at eight bytes. Both are replaced here, the
counter by an open-addressed map over occupied pairs and the symbol by a fixed
byte array with an explicit length, so the same iterative local search can fill a
sixteen-bit table with symbols as long as OnPair's.

The trained table is handed to OnPair's own encoder through a new train-free
entry point, so the parsing pass and the decode kernel are shared and only the
table construction differs. That makes the ratio and decode columns attributable
to the table alone.

The round-trip verifier now gates rather than reports, returning non-zero on any
failure, and it decodes every FSST16 configuration twice, once through the
whole-column path and once through the bit-packed path at the width the trained
table needs, because a table of a few hundred tokens exercises a much narrower
packed loop than OnPair ever reaches.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants